Modernising an Enterprise Front End Stack
Almost any large enterprise struggles to keep their front-end code maintainable — and so did we! CSS and Javascript code bases grow large and unwieldy quite easily. With our component library, we were able to overcome these challenges. We now have a common style guide with reusable components that our business can drag and drop using our content management system. New releases are tested automatically. This article describes our journey from an early version of LUX towards our new React-based LUX 2.0.
Let’s start by describing the tool set we use in-house to deliver UI components:
An ecosystem that allows business and IT to manage, develop and deploy UX components into production.
It gives us:
- Reusable UI designs
- Quicker release cycles
- Quality Control
- Mobile Responsiveness
- Less duplicated effort between developers and designers
It allows both business and IT to have a live playground where they can rapidly prototype new UX designs and deploy into production once it’s ready. We call it LUX — Living UX.
Singtel and Optus started the project in 2015 and it was the team’s first framework. We started by prototyping a handful of frameworks and Ractive was chosen. You have probably never heard of Ractive. For simplicity, think of it as a React alternative. It just did not get as much traction in the community as React.
There were a lot of learnings and exciting times as we watched LUX grow throughout the company. However, it grew too fast. Over time, as we built this in-house framework, we started to face problems as our applications grew with business needs, such as performance impacts, we were not able to scale, components lost their reusability, and our release cycles were slowed down because of the lack of quality control tools.
And so after three years, we have collected our learnings and experiences to give birth to LUX 2.0. Here is our approach to tackling the problems we faced and what we are learning on this journey.
Going declarative with React
We had the opportunity to start from scratch with the UI and so to look at solving our modularity concerns, we looked at using React. React’s composition pattern allows us to isolate components with their own state, and by lifting state up to a container level, our engineers can easily build complex UI by composing smaller and reusable components. With React, it became clearer to understand what is reusable presentation components and what is stateful components. This has refrained us from duplicating the 800 components from the processor version of the framework and we have a declarative way of thinking how to put the UI together.
Our components will now become reusable, shareable between apps, that can be tested by isolation.
Architecting AEM with React
Our pages are generated using Adobe Experience Manager that allows the business to drag and drop different UI components (think of them as business Widgets) on a large collection of pages. These CMS components render small React apps on run-time that can talk to each other using a custom layer we built. With this layer, we added Redux to manage one global store connected to the multiple React apps placed on the page. By having one global store, each widget is name-spaced and have the ability to talk to each other. The page acts like one large React app.
This approach works well for us. It allowed us to push LUX 2.0 early into production without any heavy modifications to AEM and no changes to LUX 1.0 were required. The next step in this area will be to get AEM to render the full page as one react app for us to enjoy the benefits of a server-side rendered application.
Eliminating boilerplate with lux-core
Our previous apps became too large because they bundled every component we have created, even if the app did not utilise them. It had a negative impact on web performance and deployment plans because every app became a fork of another that cannot be merged back. This has also affected modularity because some apps had duplicated effort of building the same components.
To solve our issue on scalability we will then split the core layer from the component layer: @lux/core and @lux/components. @lux/core is treated as a no-build-configuration tool that contains all the bootstrap utilities, like the tools and scripts used for rendering, testing, building and deploying the app — the elements that the developers should not have to worry about every time when we build a new app. @lux/components contains our Singtel base components, both mobile and web.
This separation simply allows us to do something like this:
For every app that we build, the developers do not have to always worry about setups of things such as redux store creation, error handling, analytics tracking. This list can grow, and when it does, we upgrade @lux/core without the apps worrying about it. If an app requires a new feature found in the core, their engineers just need to bump up their dependency versions, test and deploy. This approach works for apps not just build in AEM, but also for our apps that we built as standalone web apps, WidgetManager has different APIs to render different sorts of applications.
Taking the No-Build-Configuration approach
Previously in the earlier version of the framework, we followed a mono-repo approach that was bundled using RequireJs (AMD approach). We found it a struggle to deploy and develop because we bundled all components required for all apps into one. We were not able to scale and we were caught in having performance issues with the asynchronous loading of modules. Our previous JS bundler almost reached 5MB unzipped, with CSS up to 1MB.
We solved this by moving our custom bundling scripts to Webpack and we now bundle only the components and libraries required for each app individually.
We even went one step further by code splitting and using React-Loadable to load our code splits when required. Our bundles have been a lot smaller and we only load the required JS for each app view.
All the Webpack bundling configuration, along with scripts required to build, test and deploy our apps are kept in @lux/core, to keep our focus on the no-build-configuration approach of LUX:
How to avoid CSS conflicts
One of the biggest problems we had in the past was having component stylings conflict with each other. With so many components on each page, we found a large number of classes being used in multiple components because they had the same styling, then as soon as one style was modified, another component would break. We had no quality control over how to manage CSS classes and styling, as every developer had a different style in writing their CSS. We also never know what components are on the page and so we needed to find an approach that gives us confidence that placing a component on a page that will not break the styling of itself or other components.
Because we are building a component library, our focus is to make sure each component is modularised where it’s styling is scoped. This is where Styled Components comes into play.
Styled Components allow us to write CSS-in-JS. An unconventional new approach that’s controversial in the JavaScript world, but we found very good use case:
- Scoped styling.
- Each styling element became a component.
- Remove the notion of having CSS class names to avoid styling conflicts.
- Allow other CSS frameworks to be on the page without conflicts.
- Can style both React Web and React Native.
The fact that we need to run LUX 2.0 on the same pages as the LUX 1.0, we need be assured that no CSS conflicts will happen due to conflicting CSS class names. Styled Components does this through CSS encapsulation — It generates unique classes for each component and we avoid specifying custom classes to our components.
Now using this new Paragraph component within other components, eg MarketingBanner we will see:
Building confidence using quality control tools
Adding quality control checks to our applications and frameworks have changed the way we build our applications and is one of the most exciting changes we have introduced to our workflow. We have added unit testing, linting, static type checking and visual regression testing.
It gives us the confidence which we did not have with the previous versions of LUX. When all the quality control checks are passing successfully, we feel safe shipping our products into production, quicker than we did it in the past.
Unit Testing
With a poor testing process in the past, the team have now learnt the importance of testing that we agreed to have a hard 80% code coverage threshold. It was a hard lesson to learn, but we have now developed a mindset of testing everything we build and we really do enjoy it. Jest and Enzyme allow us to unit test with ease. We can also test Styled Component’s CSS at different viewports.
Visual Regression Testing
Having a large list of UI components, another problem we had in the past was the number of defects that were raised related to CSS. We have already solved CSS encapsulation but we wanted to do a little bit more in that we don’t break anything visually with each production release.
By writing a visual regression tool using Puppeteer, we can see with each commit how many components are visually affected by each code change. If a change affects other components and it’s an expected change, we push the “Approve” button and update the reference snapshots.
Committing the reference snapshots in our repository allows us to see a diff in the Pull Requests for all developers to take notice of the visual changes made.
Code Styling
The advantage of Prettier is that it’s opinionated and when it formats your code base, the code becomes easier to read and understand as it follows one code style format. No more debates over code styles.
To an engineer, the benefit is not only what Prettier does to their own individual code, but what it does to the rest of the team’s code as a whole. The advantage comes to making it easier to understand the rest of the code base.
Pull Requests and code review sessions become less clogged with comments regarding code styles. Less time is spent in understanding code.
Prettier is hooked up to our pre-commit git hooks, so as soon as you stage your changes, it reformats your code to a consistent code style guide.
On top of Prettier, we have added ESLint for our JS and StyleLint for our CSS. StyleLint makes sure we do not add additional colour hex values or padding/margin values that do not match our style guide. It enforces our developers to be using our theme prop passed down from @lux/components. Strict styling from our theme configuration.
Static Type Checking
We’ve introduced static type checking at Singtel by implementing Flow. This enables us to catch errors earlier before running the code. We chose Flow over other similar tools because firstly our framework and applications are built using React so we know they go hand in hand now and in the future, plus the comment type interface allows us to easily enable Flow on any of our source code without a rewrite.
It also adds a form of documentation to our source code which gives us confidence and understanding of the code if we are to ever refactor any pieces of code.
Putting it all together in a style guide
LUX generates our Online Style Guide. It is our source of truth for our components. Think of it as our design language system, it outlines design aspects such as:
- Colours
- Iconography
- Layout Grid System
- Typography
- UI Components
It is a playground that allows development teams and business units to have a visual understanding of what components are available to use in any apps built for Singtel. It is also used by in-house and external marketing teams building promotion pages on our website.
The components found in the online style guide can be rendered in React Web and React Native, by taking advantage of primitive components. When a component can be rendered in both web and native apps, we deal with less code to manage, saves time and effort. The business also benefits from having one engineer team building the one product across multiple platforms.
Migrating 800 components across to React
The plan is not to move across the old 800 components, we believe that amount is too much and it is a sign that we did not do modularity or reusable components correctly. While working with the UX team, product by product we standardise components and rebuild a new LUX 2.0 variation. We then track our progress using a website tool that is hooked up to our CI stack.
When a new component is pushed into the master branch of our component library, our “LUX Progress” app builds a new copy — it updates us on the progress of moving components across.
It’s also used as an indication of which components we have already moved across and which ones we decide not to move across (either because we learnt that they should not be in a component library, or another existing component can do the same thing).
What’s Next For Singtel Digital Technology?
It’s been a fun and exciting couple of months since we push this framework into production! We now have a handful of apps built using LUX with plenty more planned. The journey is not over yet and we have plenty more to do to get where we want to be. Our next steps involve not just the technology side of this transformation but also within the engineering team and wider organisation:
- Align the design and engineering team to build one design system.
- Improve development through practices.
- Improve web performance speeds.
- Faster production deployment times.
If you’re in Singapore and enjoy Front End development, let’s go grab a coffee! We’d love to hear your thoughts on the Web and its emerging technologies.